﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;
using System.Globalization;
using System.Diagnostics;
using System.DirectoryServices;
using System.ComponentModel;
using Novell.Directory.Ldap;
using System.Collections;

namespace Novell.DirectoryServices.Linq
{

    /// <summary>
    /// LINQ query provider for Directory Services.
    /// </summary>
    public class DirectoryQueryProvider : IQueryProvider
    {
        internal struct ExecutionParameters
        {
            private bool _singleResult;
            private bool _throwExceptionIfNoResults;
            private int _resultIndex;


            public ExecutionParameters(bool singleResult, bool throwExceptionIfNoResults)
            {
                _singleResult = singleResult;
                _throwExceptionIfNoResults = throwExceptionIfNoResults;
                _resultIndex = 1;
            }
            public bool SingleResult
            {
                get { return _singleResult; }
                set { _singleResult = value; }
            }
            public bool ThrowExceptionIfNoResults
            {
                get { return _throwExceptionIfNoResults; }
                set { _throwExceptionIfNoResults = value; }
            }
            public int ResultIndex
            {
                get { return _resultIndex; }
                set { _resultIndex = value; }
            }
        }
        string _query;
        IDirectorySource _source;
        Type _originalType;
        #region Projection information

        private HashSet<string> _properties = new HashSet<string>();
        private Delegate _project;
        private ExecutionParameters _execParams = new ExecutionParameters(false, true);
        #endregion
        #region CreateQuery implementation

        public IQueryable CreateQuery(Expression expression)
        {
            Type elementType = TypeSystem.GetElementType(expression.Type);
            try
            {
                return (IQueryable)Activator.CreateInstance(typeof(DirectoryQuery<>).MakeGenericType(elementType), new object[] { this, expression });
            }
            catch (TargetInvocationException tie)
            {
                throw tie.InnerException;
            }
        }

        /// <summary>
        /// Constructs an IQueryable object that can evaluate the query represented by the specified expression tree. 
        /// </summary>
        /// <param name="expression">Expression representing the LDAP query.</param>
        /// <typeparam name="TElement">The type of the elements of the IQueryable that is returned.</typeparam>
        /// <returns>IQueryable object that can evaluate the query represented by the specified expression tree.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            return new DirectoryQuery<TElement>(expression);
        }

        #endregion

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
        public TResult Execute<TResult>(Expression expression)
        {
            return (TResult)Execute(expression);
        }
        #region Direct execution

        public object Execute(Expression expression)
        {
            Parse(expression);

            DirectorySchemaAttribute[] attr = (DirectorySchemaAttribute[])_originalType.GetCustomAttributes(typeof(DirectorySchemaAttribute), false);
            if (attr == null || attr.Length == 0)
                throw new InvalidOperationException("Missing schema mapping attribute.");
            string classQuery = GetClassQuery(attr);

            //TODO: Determine any optimizations that can be done based on _execParams.
            string query = !string.IsNullOrEmpty(_query) ? String.Format(CultureInfo.InvariantCulture,"(&{0}{1})", classQuery, _query) : classQuery;
            
            LdapSearchResults results = _source.Context.Connection.Search(_source.Root, (int)_source.Scope
                , query
                , _properties.ToArray(), false);

            //if (_source.Log != null)
            //    _source.Log.WriteLine(s.Filter);

            //Load the LdapObjectReader with the results of the query.
            IEnumerable e = (IEnumerable) Activator.CreateInstance(
                typeof(LdapObjectReader<>).MakeGenericType(_originalType)
                , _source, results, _project, _originalType);

            //Determine how to return the results.
            if (_execParams.SingleResult)
            {
                object ret = null;
                IEnumerator en = e.GetEnumerator();

                if (_execParams.ResultIndex == 1)//get the first one, this is an optimization
                {
                    if (en.MoveNext())
                    {
                        ret = en.Current;
                    }
                }
                else if (_execParams.ResultIndex == -1) //Get the last item
                {
                    //TODO: Make this more efficient
                    object o = null;
                    while (en.MoveNext())
                    {
                        o = en.Current;
                    }
                    ret = o;
                }
                else if (_execParams.ResultIndex > 0)//Get the specific index.
                {
                    int i = 0;
                    while (en.MoveNext())
                    {
                        if (++i == _execParams.ResultIndex)
                        {
                            ret = en.Current;
                            break;
                        }
                    }
                }


                if (ret == null && _execParams.ThrowExceptionIfNoResults)
                {
                    throw new ObjectNotFoundException("Object not found for filter: " + query);
                }

                return ret;
            }
            else
            {
                //Return the enumeration.  This is the simplest.
                return e;
            }
        }

        #endregion

        #region Query parsing

        /// <summary>
        /// Parses the specified query expression tree.
        /// </summary>
        /// <param name="ex"></param>
        public void Parse(Expression ex)
        {

            var ce = ex as ConstantExpression;
            var mce = ex as MethodCallExpression;

            if (ce != null)
            {
                _source = ce.Value as IDirectorySource;
                _originalType = _source.OriginalType;
            }
            else if (mce != null)
            {
                //
                // Should be extension methods on Queryable.
                //
                if (mce.Method.DeclaringType != typeof(Queryable))
                    throw new NotSupportedException("Detected invalid top-level method-call.");

                Parse(mce.Arguments[0]);

                //
                // First parameter to the method call represents the (unary) lambda in LINQ style.
                // E.g. (user => user.Name == "Bart") for a Where  clause
                //      (user => new { user.Name })   for a Select clause
                //
                
                switch (mce.Method.Name)
                {
                    //
                    // Builds the query LDAP expression.
                    //
                    case "Where":
                        BuildPredicate(((UnaryExpression)mce.Arguments[1]).Operand as LambdaExpression);
                        break;
                    //
                    // Builds the projection and filters the required properties.
                    //
                    case "Select":
                        BuildProjection(((UnaryExpression)mce.Arguments[1]).Operand as LambdaExpression);
                        _execParams.SingleResult = false;
                        _execParams.ThrowExceptionIfNoResults = false;
                        break;
                    case "First":
                        _execParams.SingleResult = true;
                        _execParams.ThrowExceptionIfNoResults = true;
                        break;
                    case "FirstOrDefault":
                        _execParams.SingleResult = true;
                        _execParams.ThrowExceptionIfNoResults = false;
                        break;
                    case "Last":
                        _execParams.SingleResult = true;
                        _execParams.ThrowExceptionIfNoResults = true;
                        _execParams.ResultIndex = -1;
                        break;
                    case "LastOrDefault":
                        _execParams.SingleResult = true;
                        _execParams.ThrowExceptionIfNoResults = false;
                        _execParams.ResultIndex = -1;
                        break;
                    case "ElementAt":
                        _execParams.SingleResult = true;
                        _execParams.ThrowExceptionIfNoResults = true;
                        _execParams.ResultIndex = -1;
                        throw new NotImplementedException("Need to parse the index from the arguments to support the ElementAt operator");
                        
                        break;
                    case "ElementAtOrDefault":
                        _execParams.SingleResult = true;
                        _execParams.ThrowExceptionIfNoResults = false;
                        _execParams.ResultIndex = -1;
                        throw new NotImplementedException("Need to parse the index from the arguments to support the ElementAtOrDefault operator");
                        
                        break;
                    case "OrderBy":
                        //TODO: Find out if the particular
                        throw new NotImplementedException("OrderBy operator not supported yet.");
                        break;
                    case "OrderByDecenting":
                        throw new NotImplementedException("OrderByDecending operator not supported yet.");
                        break;
                    default:
                        throw new NotSupportedException("Unsupported query operator: " + mce.Method.Name);
                        
                }

            }
            else
                throw new NotSupportedException("Invalid expression node detected.");
            
            
        }

        #region Projection support

        /// <summary>
        /// Helper method for projection clauses (Select).
        /// </summary>
        /// <param name="p">Lambda expression representing the projection.</param>
        private void BuildProjection(LambdaExpression p)
        {
            //
            // Store projection information including the compiled lambda for subsequent execution
            // and a minimal set of properties to be retrieved (improves efficiency of queries).
            //
            _project = p.Compile();

            //
            // Original type is kept for reflection during querying.
            //
            _originalType = p.Parameters[0].Type;

            //
            // Support for (anonymous) type initialization based on "member init expressions".
            //
            MemberInitExpression mi = p.Body as MemberInitExpression;
            if (mi != null)
            {
                Queue<MemberBinding> bindings = new Queue<MemberBinding>();
                foreach (MemberBinding b in mi.Bindings)
                    bindings.Enqueue(b);

                while (bindings.Count > 0)
                {
                    MemberBinding b = bindings.Dequeue();

                    MemberAssignment ma;
                    MemberMemberBinding mmb;
                    MemberListBinding mlb;

                    if ((ma = b as MemberAssignment) != null)
                        FindProperties(ma.Expression);
                    else if ((mmb = b as MemberMemberBinding) != null)
                        foreach (MemberBinding mb in mmb.Bindings)
                            bindings.Enqueue(mb);
                    else if ((mlb = b as MemberListBinding) != null)
                        foreach (ElementInit ie in mlb.Initializers)
                            foreach (Expression arg in ie.Arguments)
                                FindProperties(arg);
                }
            }
            //
            // Support for identity projections (e.g. user => user), getting all properties back.
            //
            else
                foreach (PropertyInfo i in _originalType.GetProperties())
                    _properties.Add(i.Name);
        }

        /// <summary>
        /// Recursive helper method to finds all required properties for projection.
        /// </summary>
        /// <param name="e">Expression to detect property uses for.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
        private void FindProperties(Expression e)
        {
            //
            // Record member accesses to properties or fields from the entity.
            //
            if (e.NodeType == ExpressionType.MemberAccess)
            {
                MemberExpression me = e as MemberExpression;
                if (me.Member.DeclaringType == _originalType)
                {
                    DirectoryAttributeAttribute[] da = me.Member.GetCustomAttributes(typeof(DirectoryAttributeAttribute), false) as DirectoryAttributeAttribute[];
                    if (da != null && da.Length != 0)
                    {
                        _properties.Add(me.Member.Name);
                    }
                    //We don't want to add properties for members that are not part of the query.
                    else
                    {
                        throw new NotImplementedException("Support for quering properties that are not ldap properties is not supported yet.");
                    }
                    //else
                    //    _properties.Add(me.Member.Name);
                }
            }
            else
            {
                BinaryExpression b;
                UnaryExpression u;
                ConditionalExpression c;
                InvocationExpression i;
                LambdaExpression l;
                ListInitExpression li;
                MemberInitExpression mi;
                MethodCallExpression mc;
                NewExpression n;
                NewArrayExpression na;
                TypeBinaryExpression tb;

                if ((b = e as BinaryExpression) != null)
                {
                    FindProperties(b.Left);
                    FindProperties(b.Right);
                }
                else if ((u = e as UnaryExpression) != null)
                {
                    FindProperties(u.Operand);
                }
                else if ((c = e as ConditionalExpression) != null)
                {
                    FindProperties(c.IfFalse);
                    FindProperties(c.IfTrue);
                    FindProperties(c.Test);
                }
                else if ((i = e as InvocationExpression) != null)
                {
                    FindProperties(i.Expression);
                    foreach (Expression ex in i.Arguments)
                        FindProperties(ex);
                }
                else if ((l = e as LambdaExpression) != null)
                {
                    FindProperties(l.Body);
                    foreach (Expression ex in l.Parameters)
                        FindProperties(ex);
                }
                else if ((li = e as ListInitExpression) != null)
                {
                    FindProperties(li.NewExpression);
                    foreach (ElementInit init in li.Initializers)
                        foreach (Expression ex in init.Arguments)
                            FindProperties(ex);
                }
                else if ((mi = e as MemberInitExpression) != null)
                {
                    FindProperties(mi.NewExpression);
                    foreach (MemberAssignment ma in mi.Bindings)
                        FindProperties(ma.Expression);
                }
                else if ((mc = e as MethodCallExpression) != null)
                {
                    FindProperties(mc.Object);
                    foreach (Expression ex in mc.Arguments)
                        FindProperties(ex);
                }
                else if ((n = e as NewExpression) != null)
                {
                    foreach (Expression ex in n.Arguments)
                        FindProperties(ex);
                }
                else if ((na = e as NewArrayExpression) != null)
                {
                    foreach (Expression ex in na.Expressions)
                        FindProperties(ex);
                }
                else if ((tb = e as TypeBinaryExpression) != null)
                {
                    FindProperties(tb.Expression);
                }
            }
        }

        #endregion

        #region Query predicate support

        /// <summary>
        /// Helper method to build the LDAP query.
        /// </summary>
        /// <param name="q">Lambda expression to be translated to LDAP.</param>
        private void BuildPredicate(LambdaExpression q)
        {
            StringBuilder sb = new StringBuilder();

            //
            // Recursive tree traversal to build the LDAP query (prefix notation).
            //
            ParsePredicate(q.Body, sb);

            _query = sb.ToString();
        }

        /// <summary>
        /// Recursive helper method for query parsing based on the given expression tree.
        /// </summary>
        /// <param name="e">Expression tree to be translated to LDAP.</param>
        /// <param name="sb">Accummulative query string used in recursion.</param>
        private void ParsePredicate(Expression e, StringBuilder sb)
        {
            BinaryExpression b;
            UnaryExpression u;
            MethodCallExpression m;

            sb.Append("(");
            //
            // Support for boolean operators & and |. Support for "raw" conditions (like equality).
            //
            if ((b = e as BinaryExpression) != null)
            {
                switch (b.NodeType)
                {
                    case ExpressionType.AndAlso:
                        sb.Append("&");
                        ParsePredicate(b.Left, sb);
                        ParsePredicate(b.Right, sb);
                        break;
                    case ExpressionType.OrElse:
                        sb.Append("|");
                        ParsePredicate(b.Left, sb);
                        ParsePredicate(b.Right, sb);
                        break;
                    default: //E.g. Equal, NotEqual, GreaterThan
                        sb.Append(GetCondition(b));
                        break;
                }
            }
            //
            // Support for boolean negation.
            //
            else if ((u = e as UnaryExpression) != null)
            {
                if (u.NodeType == ExpressionType.Not)
                {
                    sb.Append("!");
                    ParsePredicate(u.Operand, sb);
                }
                else
                    throw new NotSupportedException("Unsupported query operator detected: " + u.NodeType);
            }
            //
            // Support for string operations.
            //
            else if ((m = e as MethodCallExpression) != null)
            {
                MemberExpression o = (m.Object as MemberExpression);
                if (m.Method.DeclaringType == typeof(string))
                {
                    switch (m.Method.Name)
                    {
                        case "Contains":
                            {
                                string value = GetStringOperandValue(m);
                                sb.AppendFormat("{0}=*{1}*", GetFieldName(o.Member), value);
                                break;
                            }
                        case "StartsWith":
                            {
                                string value = GetStringOperandValue(m);
                                sb.AppendFormat("{0}={1}*", GetFieldName(o.Member), value);
                                break;
                            }
                        case "EndsWith":
                            {
                                string value = GetStringOperandValue(m);
                                sb.AppendFormat("{0}=*{1}", GetFieldName(o.Member), value);
                                break;
                            }
                        default:
                            throw new NotSupportedException("Unsupported string filtering query expression detected. Cannot translate to LDAP equivalent.");
                    }
                }
                else
                    throw new NotSupportedException("Unsupported query expression detected. Cannot translate to LDAP equivalent.");
            }
            else
                throw new NotSupportedException("Unsupported query expression detected. Cannot translate to LDAP equivalent.");
            sb.Append(")");
        }

        /// <summary>
        /// Helper method to get the first string-valued parameter of a method call expression.
        /// </summary>
        /// <param name="m">Method call expression to get the string-valued parameter from.</param>
        /// <returns>First parameter string value.</returns>
        private static string GetStringOperandValue(MethodCallExpression m)
        {
            Debug.Assert(m.Arguments.Count == 1);

            string value = Expression.Lambda(m.Arguments[0]).Compile().DynamicInvoke() as string;
            if (value == null)
                throw new NotSupportedException(m.Method.Name + " can only be used with string operands.");

            return value;
        }

        /// <summary>
        /// Helper method to translate conditions to LDAP filters.
        /// </summary>
        /// <param name="e">Conditional expression to be translated to an LDAP filter.</param>
        /// <returns>String representing the condition in LDAP.</returns>
        private string GetCondition(BinaryExpression e)
        {
            string val, attrib;

            bool neg;

            //
            // Find the order of the operands in the binary expression. At least one should refer to the entity type.
            //
            if (e.Left is MemberExpression && ((MemberExpression)e.Left).Member.DeclaringType == _originalType)
            {
                neg = false;

                attrib = GetFieldName(((MemberExpression)e.Left).Member);
                val = Expression.Lambda(e.Right).Compile().DynamicInvoke().ToString();
            }
            else if (e.Right is MemberExpression && ((MemberExpression)e.Right).Member.DeclaringType == _originalType)
            {
                neg = true;

                attrib = GetFieldName(((MemberExpression)e.Right).Member);
                val = Expression.Lambda(e.Left).Compile().DynamicInvoke().ToString();
            }
            else
                throw new NotSupportedException("A filtering expression should contain an entity member selection expression.");

            //
            // Normalize some common characters that cannot be used in LDAP filters.
            //
            val = val.ToString().Replace("(", "0x28").Replace(")", "0x29").Replace(@"\", "0x5c");

            //
            // Determine the operator and swap the operandi if necessary (LDAP requires a field=value order).
            //
            switch (e.NodeType)
            {
                case ExpressionType.Equal:
                    return String.Format(CultureInfo.InvariantCulture, "{0}={1}", attrib, val);
                case ExpressionType.NotEqual:
                    return String.Format(CultureInfo.InvariantCulture, "!({0}={1})", attrib, val);
                case ExpressionType.GreaterThanOrEqual:
                    if (!neg)
                        return String.Format(CultureInfo.InvariantCulture, "{0}>={1}", attrib, val);
                    else
                        return String.Format(CultureInfo.InvariantCulture, "{0}<={1}", attrib, val);
                case ExpressionType.GreaterThan:
                    if (!neg)
                        return String.Format(CultureInfo.InvariantCulture, "&({0}>={1})(!({0}={1}))", attrib, val);
                    else
                        return String.Format(CultureInfo.InvariantCulture, "&({0}<={1})(!({0}={1}))", attrib, val);
                case ExpressionType.LessThanOrEqual:
                    if (!neg)
                        return String.Format(CultureInfo.InvariantCulture, "{0}<={1}", attrib, val);
                    else
                        return String.Format(CultureInfo.InvariantCulture, "{0}>={1}", attrib, val);
                case ExpressionType.LessThan:
                    if (!neg)
                        return String.Format(CultureInfo.InvariantCulture, "&({0}<={1})(!({0}={1}))", attrib, val);
                    else
                        return String.Format(CultureInfo.InvariantCulture, "&({0}>={1})(!({0}={1}))", attrib, val);
                default:
                    throw new NotSupportedException("Unsupported filtering operator detected: " + e.NodeType);
            }
        }

        /// <summary>
        /// Gets the mapped field name for the specified entity type member.
        /// </summary>
        /// <param name="member">Entity type member.</param>
        /// <returns>Mapped field name. If no mapping is applied to the specified member, the member's name will be returned (default mapping).</returns>
        private static string GetFieldName(MemberInfo member)
        {
            DirectoryAttributeAttribute[] da = member.GetCustomAttributes(typeof(DirectoryAttributeAttribute), false) as DirectoryAttributeAttribute[];
            if (da != null && da.Length != 0 && da[0] != null)
            {
                return da[0].Attribute;
            }
            else
                return member.Name;
        }

        /// <summary>
        /// Returns the portion of the query that filters based on the object class.
        /// </summary>
        /// <param name="attr">An array of DirectorySchemaAttribute attributes specifying the object class for a class.</param>
        /// <returns>An ldap filter representing the object classes passed in.</returns>
        private static string GetClassQuery(DirectorySchemaAttribute[] attr)
        {
            string classQuery = "";
            if (attr.Count() > 1)
            {
                classQuery += "(&";
                foreach (DirectorySchemaAttribute att in attr)
                {
                    if (att.Search)
                    {
                        classQuery += string.Format(CultureInfo.InvariantCulture,"(objectClass={0})", att.Schema);
                    }
                }
                classQuery += ")";
            }
            else
            {
                classQuery = String.Format(CultureInfo.InvariantCulture, "(objectClass={0})", attr[0].Schema);
            }
            return classQuery;
        }
        #endregion

        #endregion

        

    }
}
